package com.fxu.framework.biz.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.fxu.framework.core.exception.MsgException;
import com.fxu.framework.core.holder.RedisHolder;
import com.fxu.framework.core.util.MapUtil;
import lombok.Builder;
import lombok.Data;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

/**
 * 微信基础类
 *
 * @author fangxu
 * @version 1.0.0
 */
public class WxUtil {
	private final static Logger logger = LoggerFactory.getLogger(WxUtil.class);

	@Data
	public static class WxUserInfo {
		// 用户的标识
		private String openid;
		// 关注状态（1是关注，0是未关注），未关注时获取不到其余信息
		private int subscribe;
		// 用户关注时间，为时间戳。如果用户曾多次关注，则取最后关注时间
		private String subscribeTime;
		// 昵称
		private String nickname;
		// 用户的性别（1是男性，2是女性，0是未知）
		private int gender;
		// 用户所在国家
		private String country;
		// 用户所在省份
		private String province;
		// 用户所在城市
		private String city;
		// 用户的语言，简体中文为zh_CN
		private String language;
		// 用户头像
		private String headimgurl;
		// 不同小程序的公用unionId
		private String unionid;
	}

	@Data
	public static class WxAccess {
		private String appid;
		private String openid;
		private String unionid;
		private String scope;
		private String access_token;
		private String refresh_token;
		private int expires_in;
		private int errcode;
		private String errmsg;
		private String session_key;
		private String mchId;
	}

	@Data
	public static class PhoneRoot {
		private int errcode;
		private String errmsg;
		private PhoneInfo phoneInfo;
	}

	@Data
	public static class PhoneInfo {
		private String phoneNumber;
		private String purePhoneNumber;
		private String countryCode;
	}

	@Data
	public static class UserInfo {
		private String nick;
		private String avatar;
	}

	public static boolean codeIsOk(String codeValue) {
		return StrUtil.isNotEmpty(codeValue) && "SUCCESS".equals(codeValue);
	}

	public static String checkWxMap(Map<String, String> resultMap) {
		if (!codeIsOk(resultMap.get("return_code"))) {
			return resultMap.get("return_msg");
		}
		if (!codeIsOk(resultMap.get("result_code"))) {
			return resultMap.get("err_code_des");
		}
		return "";
	}

	/**
	 * h5获取微信用户信息
	 * @param accessToken 钥匙
	 * @param openId      微信openid
	 * @return 微信用户信息
	 */
	public static WxUserInfo h5UserInfo(String accessToken, String openId) {
		final String url = "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}&lang=zh_CN";
		// logger.debug("url===>", StrUtil.format(url, accessToken, openId));
		try {
			String json = HttpRequest.get(StrUtil.format(url, accessToken, openId)).execute().body();
			return JSONUtil.toBean(json, WxUserInfo.class);
		} catch (Exception e) {
			throw new MsgException("根据accessToken查询微信userInfo错误!e=" + e);
		}
	}

	/**
	 * <p>获取用户信息</p>
	 * @param accessToken 钥匙
	 * @param openId      微信openid
	 * @return 微信用户信息
	 */
	public static WxUserInfo getUserInfo(String accessToken, String openId) {
		final String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={}&openid={}&lang=zh_CN";
		try {
			String body = HttpUtil.get(StrUtil.format(url, accessToken, openId));
			return JSONUtil.toBean(body, WxUserInfo.class);
		} catch (Exception e) {
			throw new MsgException("根据accessToken查询微信userInfo错误!e=" + e);
		}
	}

	/**
	 * 微信APP获取访问权限
	 * @param isApp     是否是小程序页面[非为h5页面]
	 * @param appId     微信appId
	 * @param appSecret 微信密钥
	 * @param code      微信APP的code
	 * @return 微信登录信息
	 */
	public static WxAccess accessToken(boolean isApp, String appId, String appSecret, String code) {
		String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code";
		if (isApp) {
			url = "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code";
			if (appId.startsWith("tt")) {
				url = "https://developer.toutiao.com/api/apps/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code";
			}
		}
		logger.debug("appAccess.querys===>{},{}", isApp, StrUtil.format(url, appId, appSecret, code));
		try {
			String json = HttpRequest.get(StrUtil.format(url, appId, appSecret, code)).execute().body();
			WxAccess wxAccess = JSONUtil.toBean(json, WxAccess.class);
			wxAccess.appid = appId;

			if (wxAccess.getErrcode() > 0) {
				throw new MsgException("错误码：" + wxAccess.getErrcode() + "；错误信息：" + wxAccess.getErrmsg());
			}
			if (StrUtil.isEmpty(wxAccess.getOpenid())) {
				throw new MsgException("获取OpenId失败！");
			}
			return wxAccess;
		} catch (Exception e) {
			throw new MsgException("根据code查询微信openId错误!e=" + e);
		}
	}

	/***
	 * <p>解密用户敏感数据获取用户信息</p>
	 * @param sessionKey 数据进行加密签名的密钥
	 * @param encryptedData 包括敏感数据在内的完整用户信息的加密数据
	 * @param iv 加密算法的初始向量
	 * @return 解密后的数据，解析失败返回null
	 */
	public static UserInfo decodeInfoMayNull(String sessionKey, String encryptedData, String iv) {
		Map<?, ?> map = decodeInfo(sessionKey, encryptedData, iv);
		if (map == null) return null;
		UserInfo userInfo = new UserInfo();
		userInfo.setNick(MapUtil.getStr(map, "nickName", ""));
		userInfo.setAvatar(MapUtil.getStr(map, "avatarUrl", ""));
		return userInfo;
	}

	/***
     * <p>解密用户敏感数据获取用户信息</p>
     * @param sessionKey 数据进行加密签名的密钥
     * @param encryptedData 包括敏感数据在内的完整用户信息的加密数据
     * @param iv 加密算法的初始向量
     * @return 解密后的数据
     */
    public static Map<?, ?> decodeInfo(String sessionKey, String encryptedData, String iv) {
		// 被加密的数据
		byte[] dataByte = Base64.decode(encryptedData);
		// 加密秘钥
		byte[] keyByte = Base64.decode(sessionKey);
		// 偏移量
		byte[] ivByte = Base64.decode(iv);
		try {
			// 如果密钥不足16位，那么就补足. 这个if 中的内容很重要
			int base = 16;
			if (keyByte.length % base != 0) {
				int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
				byte[] temp = new byte[groups * base];
				Arrays.fill(temp, (byte) 0);
				System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
				keyByte = temp;
			}
			// 初始化
			Security.addProvider(new BouncyCastleProvider());
			Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
			SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
			AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
			parameters.init(new IvParameterSpec(ivByte));
			cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
			byte[] resultByte = cipher.doFinal(dataByte);
			if (null != resultByte && resultByte.length > 0) {
				String result = new String(resultByte, StandardCharsets.UTF_8);
				return JSONUtil.toBean(result, Map.class);
			}
		} catch (Exception e) {
			throw new MsgException("根据openId查询用户信息错误!e=" + e);
		}
		return null;
    }

	/**
	 * 获取访问权限
	 * @param appId     微信appId
	 * @param appSecret 微信密钥
	 * @return 获得微信访问密钥
	 */
	public static String getWxAccessToken(String appId, String appSecret) {
		// 有次数限制，必须做redis缓存
		String accessToken = (String) RedisHolder.getRedisTemplate().opsForValue().get("WX_ACCESS_TOKEN_" + appId);
		if (StrUtil.isEmpty(accessToken)) {
			accessToken = getWxAccessTokenNoRedis(appId, appSecret);
			RedisHolder.getRedisTemplate().opsForValue().set("WX_ACCESS_TOKEN_" + appId, accessToken, 60, TimeUnit.SECONDS);
		}
		return accessToken;
	}

	/**
	 * 获取访问权限
	 * @param appId     微信appId
	 * @param appSecret 微信密钥
	 * @return 返回微信访问密钥没有redis缓存
	 */
	public static String getWxAccessTokenNoRedis(String appId, String appSecret) {
		String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
		// 兼容头条小程序
		if (appId.startsWith("tt")) {
			url = "https://developer.taotiao.com/api/apps/token?grant_type=client_credential&appid={}&secret={}";
		}
		String json = HttpRequest.get(StrUtil.format(url, appId, appSecret)).execute().body();
		Map<?, ?> map = JSONUtil.toBean(json, Map.class);
		if (MapUtil.getInt(map, "errcode", 0) > 0) {
			throw new MsgException("getWxAccessToken错误!map=" + map);
		}
		return map.get("access_token") == null ? "" : (String) map.get("access_token");
	}

	/**
	 * <p>
	 * 获取Ticket
	 * </p>
	 * 
	 * @param accessToken 微信许可密钥 getWxAccessToken获取
	 * @return Ticket
	 */
	public static String getTicket(String accessToken) {
		// 有次数限制，必须做redis缓存
		String ticket = (String) RedisHolder.getRedisTemplate().opsForValue().get("WX_TICKET_" + accessToken);
		if (StrUtil.isEmpty(ticket)) {
			String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi";
			String json = HttpRequest.get(url).execute().body();
			Map<?, ?> map = JSONUtil.toBean(json, Map.class);
			if (MapUtil.getInt(map, "errcode", 0) > 0) {
				throw new MsgException("getTicket错误!map=" + map);
			}
			ticket = map.get("ticket") == null ? "" : (String) map.get("ticket");
			RedisHolder.getRedisTemplate().opsForValue().set("WX_TICKET_" + accessToken, ticket, 60, TimeUnit.SECONDS);
		}
		return ticket;
	}

	@Data
	@Builder
	public static class H5ShareSign {
		private String sgture;
		private String noncestr;
		private String timestamp;
		private String appid;
	}

	/**
	 * <p> 获取分享签名 </p>
	 * @param ticket getTicket获取的Ticket
	 * @param url    分享url
	 * @param appId  微信appId
	 * @return 签名Map
	 */
	public static H5ShareSign h5ShareSign(String ticket, String url, String appId) {
		String nonce_str = UUID.randomUUID().toString();
		String timestamp = Long.toString(System.currentTimeMillis() / 1000);
		// 注意这里参数名必须全部小写，且必须有序
		String string1 = StrUtil.format("jsapi_ticket={}&noncestr={}&timestamp={}&url={}", ticket, nonce_str, timestamp, url);
		String signature;
		try {
			MessageDigest crypt = MessageDigest.getInstance("SHA-1");
			crypt.reset();
			crypt.update(string1.getBytes(StandardCharsets.UTF_8));
			signature = byteToHex(crypt.digest());
		} catch (NoSuchAlgorithmException e) {
			throw new MsgException("签名错误!e=" + e);
		}
		return H5ShareSign.builder().sgture(signature).noncestr(nonce_str).timestamp(timestamp).appid(appId).build();
	}

	/**
	 * <p> 获取分享签名 </p>
	 * @param ticket getTicket获取的Ticket
	 * @param url    分享url
	 * @param appId  微信appId
	 * @return 签名Map
	 */
	public static Map<String, String> shareSign(String ticket, String url, String appId) {
		String nonce_str = UUID.randomUUID().toString();
		String timestamp = Long.toString(System.currentTimeMillis() / 1000);
		// 注意这里参数名必须全部小写，且必须有序
		String string1 = StrUtil.format("jsapi_ticket={}&noncestr={}&timestamp={}&url={}", ticket, nonce_str, timestamp, url);
		String signature;
		try {
			MessageDigest crypt = MessageDigest.getInstance("SHA-1");
			crypt.reset();
			crypt.update(string1.getBytes(StandardCharsets.UTF_8));
			signature = byteToHex(crypt.digest());
		} catch (NoSuchAlgorithmException e) {
			throw new MsgException("签名错误!e=" + e);
		}
		Map<String, String> ret = new HashMap<>();
// 		ret.put("url", url);
// 		ret.put("ticket", ticket);
		ret.put("sgture", signature); // 签名
		ret.put("timestamp", timestamp); // 时间戳
		ret.put("noncestr", nonce_str); // 随即串
		ret.put("appid", appId); // appID
		return ret;
	}

	// 生成签名
	public static String byteToHex(final byte[] hash) {
		Formatter formatter = new Formatter();
		for (byte b : hash) {
			formatter.format("%02x", b);
		}
		String result = formatter.toString();
		formatter.close();
		return result;
	}

	/**
	 * 发送小程序信息
	 * @param openAppId     openId的appId
	 * @param openAppSecret openId的密钥
	 * @param toOpenId      发送给用户的openId
	 * @param templateId    模板ID
	 * @param data          发送内容替换
	 * @param page          页码
	 * @return 正常返回null,错误返回错误信息
	 */
	public static String sendNewMiniAppMessage(String openAppId, String openAppSecret, String toOpenId,
			String templateId, Map<String, Object> data, String page) {
		String json = getNewMiniAppMessageJson(toOpenId, templateId, data, page);
		String accessToken = getWxAccessToken(openAppId, openAppSecret);
		if (!StrUtil.isEmpty(accessToken)) {
			String sendUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken; // 微信发送消息地址
			Map<?, ?> result = JSONUtil.toBean(cn.hutool.http.HttpUtil.post(sendUrl, json), Map.class);
			if (MapUtil.getInt(result, "errcode", 1) != 0) {
				logger.error("json={}", json);
				return (String) result.get("errmsg");
			}
		}
		return null;
	}

	private static String getNewMiniAppMessageJson(String toOpenId, String templateId, Map<String, Object> data,
			String page) {
		Map<String, Map<String, Object>> newData = new HashMap<>();
		for (Entry<String, Object> entry : data.entrySet()) {
			newData.put(entry.getKey(), MapUtil.put("value", entry.getValue()).build());
		}
		return JSONUtil.parseObj(MapUtil.put("touser", toOpenId).put("template_id", templateId).put("page", page)
				.put("data", newData).build()).toString();
	}

	/**
	 * 发送公众号信息
	 * @param mpAppId       openId的appId
	 * @param mpSecret      openId的密钥
	 * @param toOpenId      发送给用户的openId
	 * @param templateId    模板ID
	 * @param jumpUrl       跳转h5链接地址
	 * @param data          发送内容替换
	 * @param miniAppId     公众号miniAppId
	 * @param miniPage      公众号miniPage
	 * @return 正常返回null,错误返回错误信息
	 */
	public static String sendNewMpMessage(String mpAppId, String mpSecret, String toOpenId, String templateId,
										  String jumpUrl, Map<String, Object> data, String miniAppId, String miniPage) {
		String json = getNewMpMessageJson(mpAppId, toOpenId, templateId, data, jumpUrl, miniAppId, miniPage);
		String accessToken = getWxAccessToken(mpAppId, mpSecret);
		if (!StrUtil.isEmpty(accessToken)) {
			String sendUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken;
			Map<?, ?> result = JSONUtil.toBean(cn.hutool.http.HttpUtil.post(sendUrl, json), Map.class);
			if (MapUtil.getInt(result, "errcode", 1) != 0) {
				return (String) result.get("errmsg");
			}
		}
		return null;
	}

	private static String getNewMpMessageJson(String mpAppId, String toOpenId, String templateId, Map<String, Object> data,
											  String url, String jumpAppId, String jumpPagePath) {
		Map<String, Map<String, Object>> newData = new HashMap<>();
		for (Entry<String, Object> entry : data.entrySet()) {
			newData.put(entry.getKey(), MapUtil.put("value", entry.getValue()).put("color", "#173177").build());
		}
		Map<String, Object> map = MapUtil.put("touser", toOpenId).put("appid", mpAppId).put("template_id", templateId).put("url", url)
				.put("data", newData).build();
		if (!StrUtil.isEmpty(jumpAppId)) {
			map.put("miniprogram", MapUtil.put("appid", jumpAppId)
					.put("pagepath", StrUtil.isEmpty(jumpPagePath) ? "" : jumpPagePath).build());
		}
		return JSONUtil.parseObj(map).toString();
	}

	/**
	 * <p>获取二维码</p> <p>接口B：适用于需要的码数量极多的业务场景</p>
	 * 
	 * @param scene 参数
	 * @param page 页面
	 * @param width 宽度
	 * @param accessToken 钥匙
	 * @return InputStream inputStream ：图片信息流
	 */
	public static InputStream getWxCode(String accessToken, String page, String scene, String width) {
		String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
		return httpsRequests(url, StrUtil.format("{\"page\":\"{}\",\"scene\":\"{}\",\"width\":\"{}\",\"is_hyaline\":true}", scene, page, width));
	}

	/**
	 * <p>获取二维码</p> <p>接口A：适用于需要的码数量极少的业务场景[总量10W条]</p>
	 * @param accessToken 钥匙
	 * @param path 页面
	 * @param width 宽度
	 * @return InputStream inputStream ：图片信息流
	 */
	public static InputStream getWxCode(String accessToken, String path, String width) {
		String url = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken;
		return httpsRequests(url, StrUtil.format("{\"path\":\"{}\",\"width\":\"{}\",\"is_hyaline\":true}", path, width));
	}

	/**
	 * 请求微信接口服务，用code换取用户手机号（每个code只能使用一次，code的有效期为5min）
	 * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html
	 * @param appId     微信appId
	 * @param appSecret 微信密钥
	 * @param code      授权手机号获得的code
	 * @return 返回纯手机号
	 */
	public static String getPhoneNumber(String appId, String appSecret, String code) {
		try {
			String accessToken = getWxAccessToken(appId, appSecret);
			String baseUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
			String jsonStr = JSONUtil.toJsonStr(cn.hutool.core.map.MapUtil.of("code", code));
			String result = HttpRequest.post(baseUrl).body(jsonStr).execute().body();
			PhoneRoot phoneRoot = JSONUtil.toBean(result, PhoneRoot.class);
			if (phoneRoot.getErrcode() == 0) {
				return phoneRoot.getPhoneInfo().getPurePhoneNumber();
			} else {
				logger.error("getPhoneNumber error! code={}, msg={}", phoneRoot.getErrcode(), phoneRoot.getErrmsg());
			}
		} catch (Exception e) {
			logger.error("WxUtil.getPhoneNumber error!", e);
		}
		return null;
	}

	/**
	 * 请求url
	 * @param requestUrl    请求url
	 * @param outputStr     输出String
	 * @return 返回http请求的输入流
	 */
	private static InputStream httpsRequests(String requestUrl, String outputStr) {
		HttpsURLConnection conn;
		InputStream inputStream;
		try {
			// 创建SSLContext对象 并使用我们指定的信任管理器初始化
			TrustManager[] tm = { new MyX509TrustManager() };
			SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
			sslContext.init(null, tm, new java.security.SecureRandom());
			// 从上述SSLContext对象中得到SSLSocketFactory对象
			SSLSocketFactory ssf = sslContext.getSocketFactory();
			URL url = new URL(requestUrl);
			conn = (HttpsURLConnection) url.openConnection();
			conn.setSSLSocketFactory(ssf);
			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setUseCaches(false);
			// 设置请求方式
			conn.setRequestMethod("POST");
			// 当outputStr 不为null时 向输出流写数据
			if (null != outputStr) {
				OutputStream outputStreams = conn.getOutputStream();
				outputStreams.write(outputStr.getBytes(StandardCharsets.UTF_8));
				outputStreams.close();
			}
			// 读取返回数据
			inputStream = conn.getInputStream();
		} catch (Exception e) {
			throw new MsgException("根据openId查询用户信息错误!e=" + e);
		}
		return inputStream;
	}

	private static class MyX509TrustManager implements X509TrustManager {
		// 检查客户端证书
		@Override
		public void checkClientTrusted(X509Certificate[] chain, String authType) {
		}

		// 检查服务器端证书
		@Override
		public void checkServerTrusted(X509Certificate[] chain, String authType) {
		}

		// 返回受信任的X509证书数组
		@Override
		public X509Certificate[] getAcceptedIssuers() {
			return null;
		}
	}
}